import { BufferGeometry, Vector3, Float32BufferAttribute, Vector2 } from 'three';

class EllipsoidGeometry extends BufferGeometry {

	private parameters: {
		radius: Vector3;
		widthSegments: number;
		heightSegments: number;
		phiStart: number;
		phiLength: number;
		thetaStart: number;
		thetaLength: number;
	};

	/**
	 * Constructs a new ellipsoid geometry.
	 *
	 * @param {Vector3} [radius=new Vector3(1, 1, 1)] - The ellipsoid radius.
	 * @param {number} [widthSegments=32] - The number of horizontal segments. Minimum value is `3`.
	 * @param {number} [heightSegments=16] - The number of vertical segments. Minimum value is `2`.
	 * @param {number} [phiStart=0] - The horizontal starting angle in radians.
	 * @param {number} [phiLength=Math.PI*2] - The horizontal sweep angle size.
	 * @param {number} [thetaStart=0] - The vertical starting angle in radians.
	 * @param {number} [thetaLength=Math.PI] - The vertical sweep angle size.
	 */
	constructor(
		radius: Vector3 = new Vector3(1, 1, 1),
		widthSegments: number = 32,
		heightSegments: number = 16,
		phiStart: number = 0,
		phiLength: number = Math.PI * 2,
		thetaStart: number = 0,
		thetaLength: number = Math.PI
	) {

		super();

		this.type = 'EllipsoidGeometry';

		this.parameters = {
			radius: radius,
			widthSegments: widthSegments,
			heightSegments: heightSegments,
			phiStart: phiStart,
			phiLength: phiLength,
			thetaStart: thetaStart,
			thetaLength: thetaLength
		};

		widthSegments = Math.max( 3, Math.floor( widthSegments ) );
		heightSegments = Math.max( 2, Math.floor( heightSegments ) );

		const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );

		let index = 0;
		const grid: number[][] = [];

		const vertex = new Vector3();
		const normal = new Vector3();

		// buffers

		const indices: number[] = [];
		const vertices: number[] = [];
		const normals: number[] = [];
		const uvs: number[] = [];

		// generate vertices, normals and uvs

		for ( let iy = 0; iy <= heightSegments; iy ++ ) {

			const verticesRow: number[] = [];

			const v = iy / heightSegments;

			// special case for the poles

			let uOffset = 0;

			if ( iy === 0 && thetaStart === 0 ) {

				uOffset = 0.5 / widthSegments;

			} else if ( iy === heightSegments && thetaEnd === Math.PI ) {

				uOffset = - 0.5 / widthSegments;

			}

			for ( let ix = 0; ix <= widthSegments; ix ++ ) {

				const u = ix / widthSegments;

				// vertex

				vertex.x = - radius.x * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
				vertex.y = radius.y * Math.cos( thetaStart + v * thetaLength );
				vertex.z = radius.z * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );

				vertices.push( vertex.x, vertex.y, vertex.z );

				// normal
				// normal.copy( ellipsoid.geodeticSurfaceNormal(vertex) ).normalize();
				normal.copy( vertex ).normalize();
				normals.push( normal.x, normal.y, normal.z );

				// uv
				let uv = this.computeTextureCoordinates(normal);
				// uvs.push( uv.x, uv.y );
				uvs.push( u + uOffset, 1 - v );

				verticesRow.push( index ++ );

			}

			grid.push( verticesRow );

		}

		// indices

		for ( let iy = 0; iy < heightSegments; iy ++ ) {

			for ( let ix = 0; ix < widthSegments; ix ++ ) {

				const a = grid[ iy ][ ix + 1 ];
				const b = grid[ iy ][ ix ];
				const c = grid[ iy + 1 ][ ix ];
				const d = grid[ iy + 1 ][ ix + 1 ];

				if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
				if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );

			}

		}

		// build geometry

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	}

	public computeTextureCoordinates(normal: Vector3): Vector2 {
        const s = Math.atan2(normal.y, normal.x) / (2 * Math.PI) + 0.5;
    	const t = Math.asin(normal.z) / Math.PI + 0.5;
        return new Vector2(s, t);
    }

	copy( source: EllipsoidGeometry ): this {

		super.copy( source );

		this.parameters = Object.assign( {}, source.parameters );

		return this;

	}

	/**
	 * Factory method for creating an instance of this class from the given
	 * JSON object.
	 *
	 * @param {Object} data - A JSON object representing the serialized geometry.
	 * @return {EllipsoidGeometry} A new instance.
	 */
	static fromJSON( data: { [key: string]: number } ): EllipsoidGeometry {

		return new EllipsoidGeometry(
			data.radius,
			data.widthSegments,
			data.heightSegments,
			data.phiStart,
			data.phiLength,
			data.thetaStart,
			data.thetaLength
		);

	}

}

export default EllipsoidGeometry;

export { EllipsoidGeometry };